package com.yanyeori.framework.configcenter.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.config.YamlPropertiesFactoryBean;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.env.EnvironmentPostProcessor;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.env.PropertiesPropertySource;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.support.PropertiesLoaderUtils;
import org.springframework.util.ResourceUtils;
import org.springframework.util.StringUtils;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Properties;

/**
 * 配置属性加解密
 *
 * @author chenkuan 2020/8/4
 */
@Slf4j
public class ConfigCenterAutoConfig implements EnvironmentPostProcessor {
    /**
     * 配置中心启用开关.
     */
    private static final String CONFIGCENTER_ENABLED = "configcenter.bootstrap.enabled";

    /**
     * 应用程序名称（必须配置）.
     */
    private static final String APPLICATION_LINE = "spring.application.name";

    /**
     * 默认的配置文件名称.
     */
    private static final String DEFAULT_NAMES = "application";

    /**
     * 配置文件的文件名，用逗号分开，只配置application-*.properties 星号代表的部分
     */
    public static final String INCLUDE_PROFILES_PROPERTY = "spring.profiles.include";

    /**
     * 激活配置文件.
     */
    public static final String ACTIVE_PROFILES_PROPERTY = "spring.profiles.active";

    /**
     * 配置文件命名空间.
     */
    public static final String NAMESPACE = "configcenter.namespace";

    /**
     * 配置文件默认命名空间.
     */
    public static final String DEFAULT_NAMESPACE = "default";

    /**
     * windows操作系统配置中心目录.
     */
    private static final String WINDOWS_CONFIGCENTER_DIR = "C://opt//configcenter//";

    /**
     * linux操作系统配置中心目录.
     */
    private static final String LINUX_CONFIGCENTER_DIR = "//opt//configcenter//";

    /**
     * 配置中心setting文件.
     */
    private static final String CONFIG_SETTING_FILE = "setting.properties";

    /**
     * 操作系统版本信息.
     */
    private static final String OS_NAME = "os.name";

    /**
     * properties文件后缀.
     */
    private static final String PROPERTIES_FILE_EXTENSION = ".properties";

    /**
     * xml文件后缀.
     */
    private static final String XML_FILE_EXTENSION = ".xml";

    /**
     * yml文件后缀.
     */
    private static final String YML_FILE_EXTENSION = ".yml";

    /**
     * yaml文件后缀.
     */
    private static final String YAML_FILE_EXTENSION = ".yaml";

    /**
     * 文件后缀集.
     */
    private static final String[] FILE_EXTENSIONS = {PROPERTIES_FILE_EXTENSION, XML_FILE_EXTENSION, YML_FILE_EXTENSION, YAML_FILE_EXTENSION};

    @Override
    public void postProcessEnvironment(ConfigurableEnvironment env, SpringApplication application) {
        try {
            String configCenterDir = this.getConfigCenterDir();
            String configSettingFileName = this.stringJoin(configCenterDir, CONFIG_SETTING_FILE);
            File configSettingFile = ResourceUtils.getFile(configSettingFileName);
            //setting配置文件不存在时,配置中心不生效.
            if (!configSettingFile.exists()) {
                return;
            }
            Properties settingProps = this.loadProperties(configSettingFile, PROPERTIES_FILE_EXTENSION);
            Boolean configEnabled = Boolean.parseBoolean(settingProps.getProperty(CONFIGCENTER_ENABLED));
            //配置中心启用开关未开启时,配置中心不启用.
            if (!Boolean.TRUE.equals(configEnabled)) {
                log.info("Config center setting.properties file does not exist.");
                return;
            }
            //统一从配置中心读取配置文件读取配置文件.
            String appName = env.getProperty(APPLICATION_LINE);
            if (StringUtils.isEmpty(appName)) {
                log.error("Application name must be set.");
                //此处也可考虑阻止应用继续启动.
                return;
            }
            log.info("Setting.properties is valid.");
            //加载共享配置.
            MutablePropertySources propertySources = env.getPropertySources();
            propertySources.addFirst(new PropertiesPropertySource(CONFIG_SETTING_FILE, settingProps));
            //加载微应用配置文件.
            String configCenterNameSpace = env.getProperty(NAMESPACE, DEFAULT_NAMESPACE);
            String appConfigDir = this.stringJoin(configCenterDir, configCenterNameSpace, "//", appName, "//");
            for (String fileExt : FILE_EXTENSIONS) {
                Properties props = this.loadProperties(appConfigDir, DEFAULT_NAMES, fileExt, propertySources);
                if (props != null) {
                    this.includeProfiles(props, appConfigDir, propertySources);
                }
            }
        } catch (IOException e) {
            log.error("Configcenter init failed. caused by :", e);
        }
    }

    /**
     * 读取include与active配置文件.
     *
     * @param defaultProps    默认属性文件.
     * @param appConfigDir    微应用配置中心文件目录.
     * @param propertySources 可变属性源集合实例.
     * @throws IOException
     */
    private void includeProfiles(Properties defaultProps, String appConfigDir, MutablePropertySources propertySources) throws IOException {
        List<String> profiles = new ArrayList<>();
        String includeProfiles = defaultProps.getProperty(INCLUDE_PROFILES_PROPERTY);
        if (StringUtils.hasText(includeProfiles)) {
            profiles.addAll(Arrays.asList(StringUtils.commaDelimitedListToStringArray(includeProfiles)));
        }
        String activeProfiles = defaultProps.getProperty(ACTIVE_PROFILES_PROPERTY);
        if (StringUtils.hasText(activeProfiles)) {
            profiles.addAll(Arrays.asList(StringUtils.commaDelimitedListToStringArray(activeProfiles)));
        }
        for (String profileName : profiles) {
            for (String fileExt : FILE_EXTENSIONS) {
                this.loadProperties(appConfigDir, this.stringJoin(DEFAULT_NAMES, "-", profileName), fileExt, propertySources);
            }
        }
    }

    /**
     * 配置中心文件根目录.
     *
     * @return 配置中心文件根目录.
     */
    private String getConfigCenterDir() {
        String os = System.getProperty(OS_NAME);
        String widowsOS = "windows";
        if (os.toLowerCase().contains(widowsOS)) {
            return WINDOWS_CONFIGCENTER_DIR;
        }
        return LINUX_CONFIGCENTER_DIR;
    }

    /**
     * 字符串连接.
     *
     * @param str 字符串数组.
     * @return 字符串.
     */
    private String stringJoin(String... str) {
        return StringUtils.collectionToDelimitedString(Arrays.asList(str), "");
    }

    /**
     * 加载配置文件.
     *
     * @param file    配置文件.
     * @param fileExt 文件后缀（如:application.yml中文件后缀为.yml）.
     * @return 上下文属性值实例对象.
     * @throws IOException
     */
    private Properties loadProperties(File file, String fileExt) throws IOException {
        FileSystemResource resource = new FileSystemResource(file);
        if (PROPERTIES_FILE_EXTENSION.endsWith(fileExt.toLowerCase()) || XML_FILE_EXTENSION.endsWith(fileExt.toLowerCase())) {
            return PropertiesLoaderUtils.loadProperties(resource);
        } else if (YML_FILE_EXTENSION.endsWith(fileExt.toLowerCase()) || YAML_FILE_EXTENSION.endsWith(fileExt.toLowerCase())) {
            YamlPropertiesFactoryBean yamlFactory = new YamlPropertiesFactoryBean();
            yamlFactory.setResources(resource);
            yamlFactory.afterPropertiesSet();
            return yamlFactory.getObject();
        }
        log.info("File type [{}] is not supported.", fileExt);
        return new Properties();
    }

    /**
     * 加载配置属性文件.
     *
     * @param appConfigDir    配置中心文件路径.
     * @param shortFileName   短文件名（如:application.yml中短文件名为application）.
     * @param fileExt         文件后缀（如:application.yml中文件后缀为.yml）.
     * @param propertySources 资源属性集合.
     * @return 属性对象实例.
     * @throws IOException
     */
    private Properties loadProperties(String appConfigDir, String shortFileName, String fileExt, MutablePropertySources propertySources) throws IOException {
        String fullFileName = this.stringJoin(appConfigDir, shortFileName, fileExt);
        File f = new File(fullFileName);
        if (f.exists()) {
            Properties props = this.loadProperties(f, fileExt);
            propertySources.addFirst(new PropertiesPropertySource(this.stringJoin(shortFileName, fileExt), props));
            return props;
        }
        return null;
    }

}
